package org.apache.lucene.index;

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import org.apache.lucene.document.Document;
import org.apache.lucene.document.Fieldable;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.store.IndexOutput;

import java.io.IOException;
import java.util.*;

/**
 * Access to the Fieldable Info file that describes document fields and whether
 * or not they are indexed. Each segment has a separate Fieldable Info file.
 * Objects of this class are thread-safe for multiple readers, but only one
 * thread can be adding documents at a time, with no other reader or writer
 * threads accessing this object.
 */
final class FieldInfos {

	static final byte IS_INDEXED = 0x1;
	static final byte STORE_TERMVECTOR = 0x2;
	static final byte STORE_POSITIONS_WITH_TERMVECTOR = 0x4;
	static final byte STORE_OFFSET_WITH_TERMVECTOR = 0x8;
	static final byte OMIT_NORMS = 0x10;
	static final byte STORE_PAYLOADS = 0x20;

	private ArrayList byNumber = new ArrayList();
	private HashMap byName = new HashMap();

	FieldInfos() {
	}

	/**
	 * Construct a FieldInfos object using the directory and the name of the
	 * file IndexInput
	 * 
	 * @param d
	 *            The directory to open the IndexInput from
	 * @param name
	 *            The name of the file to open the IndexInput from in the
	 *            Directory
	 * @throws IOException
	 */
	FieldInfos(Directory d, String name) throws IOException {
		IndexInput input = d.openInput(name);
		try {
			read(input);
		} finally {
			input.close();
		}
	}

	/**
	 * Returns a deep clone of this FieldInfos instance.
	 */
	public Object clone() {
		FieldInfos fis = new FieldInfos();
		final int numField = byNumber.size();
		for (int i = 0; i < numField; i++) {
			FieldInfo fi = (FieldInfo) ((FieldInfo) byNumber.get(i)).clone();
			fis.byNumber.add(fi);
			fis.byName.put(fi.name, fi);
		}
		return fis;
	}

	/** Adds field info for a Document. */
	public void add(Document doc) {
		List fields = doc.getFields();
		Iterator fieldIterator = fields.iterator();
		while (fieldIterator.hasNext()) {
			Fieldable field = (Fieldable) fieldIterator.next();
			add(field.name(), field.isIndexed(), field.isTermVectorStored(),
					field.isStorePositionWithTermVector(), field
							.isStoreOffsetWithTermVector(), field
							.getOmitNorms());
		}
	}

	/**
	 * Add fields that are indexed. Whether they have termvectors has to be
	 * specified.
	 * 
	 * @param names
	 *            The names of the fields
	 * @param storeTermVectors
	 *            Whether the fields store term vectors or not
	 * @param storePositionWithTermVector
	 *            treu if positions should be stored.
	 * @param storeOffsetWithTermVector
	 *            true if offsets should be stored
	 */
	public void addIndexed(Collection names, boolean storeTermVectors,
			boolean storePositionWithTermVector,
			boolean storeOffsetWithTermVector) {
		Iterator i = names.iterator();
		while (i.hasNext()) {
			add((String) i.next(), true, storeTermVectors,
					storePositionWithTermVector, storeOffsetWithTermVector);
		}
	}

	/**
	 * Assumes the fields are not storing term vectors.
	 * 
	 * @param names
	 *            The names of the fields
	 * @param isIndexed
	 *            Whether the fields are indexed or not
	 * 
	 * @see #add(String, boolean)
	 */
	public void add(Collection names, boolean isIndexed) {
		Iterator i = names.iterator();
		while (i.hasNext()) {
			add((String) i.next(), isIndexed);
		}
	}

	/**
	 * Calls 5 parameter add with false for all TermVector parameters.
	 * 
	 * @param name
	 *            The name of the Fieldable
	 * @param isIndexed
	 *            true if the field is indexed
	 * @see #add(String, boolean, boolean, boolean, boolean)
	 */
	public void add(String name, boolean isIndexed) {
		add(name, isIndexed, false, false, false, false);
	}

	/**
	 * Calls 5 parameter add with false for term vector positions and offsets.
	 * 
	 * @param name
	 *            The name of the field
	 * @param isIndexed
	 *            true if the field is indexed
	 * @param storeTermVector
	 *            true if the term vector should be stored
	 */
	public void add(String name, boolean isIndexed, boolean storeTermVector) {
		add(name, isIndexed, storeTermVector, false, false, false);
	}

	/**
	 * If the field is not yet known, adds it. If it is known, checks to make
	 * sure that the isIndexed flag is the same as was given previously for this
	 * field. If not - marks it as being indexed. Same goes for the TermVector
	 * parameters.
	 * 
	 * @param name
	 *            The name of the field
	 * @param isIndexed
	 *            true if the field is indexed
	 * @param storeTermVector
	 *            true if the term vector should be stored
	 * @param storePositionWithTermVector
	 *            true if the term vector with positions should be stored
	 * @param storeOffsetWithTermVector
	 *            true if the term vector with offsets should be stored
	 */
	public void add(String name, boolean isIndexed, boolean storeTermVector,
			boolean storePositionWithTermVector,
			boolean storeOffsetWithTermVector) {

		add(name, isIndexed, storeTermVector, storePositionWithTermVector,
				storeOffsetWithTermVector, false);
	}

	/**
	 * If the field is not yet known, adds it. If it is known, checks to make
	 * sure that the isIndexed flag is the same as was given previously for this
	 * field. If not - marks it as being indexed. Same goes for the TermVector
	 * parameters.
	 * 
	 * @param name
	 *            The name of the field
	 * @param isIndexed
	 *            true if the field is indexed
	 * @param storeTermVector
	 *            true if the term vector should be stored
	 * @param storePositionWithTermVector
	 *            true if the term vector with positions should be stored
	 * @param storeOffsetWithTermVector
	 *            true if the term vector with offsets should be stored
	 * @param omitNorms
	 *            true if the norms for the indexed field should be omitted
	 */
	public void add(String name, boolean isIndexed, boolean storeTermVector,
			boolean storePositionWithTermVector,
			boolean storeOffsetWithTermVector, boolean omitNorms) {
		add(name, isIndexed, storeTermVector, storePositionWithTermVector,
				storeOffsetWithTermVector, omitNorms, false);
	}

	/**
	 * If the field is not yet known, adds it. If it is known, checks to make
	 * sure that the isIndexed flag is the same as was given previously for this
	 * field. If not - marks it as being indexed. Same goes for the TermVector
	 * parameters.
	 * 
	 * @param name
	 *            The name of the field
	 * @param isIndexed
	 *            true if the field is indexed
	 * @param storeTermVector
	 *            true if the term vector should be stored
	 * @param storePositionWithTermVector
	 *            true if the term vector with positions should be stored
	 * @param storeOffsetWithTermVector
	 *            true if the term vector with offsets should be stored
	 * @param omitNorms
	 *            true if the norms for the indexed field should be omitted
	 * @param storePayloads
	 *            true if payloads should be stored for this field
	 */
	public FieldInfo add(String name, boolean isIndexed,
			boolean storeTermVector, boolean storePositionWithTermVector,
			boolean storeOffsetWithTermVector, boolean omitNorms,
			boolean storePayloads) {
		FieldInfo fi = fieldInfo(name);
		if (fi == null) {
			return addInternal(name, isIndexed, storeTermVector,
					storePositionWithTermVector, storeOffsetWithTermVector,
					omitNorms, storePayloads);
		} else {
			if (fi.isIndexed != isIndexed) {
				fi.isIndexed = true; // once indexed, always index
			}
			if (fi.storeTermVector != storeTermVector) {
				fi.storeTermVector = true; // once vector, always vector
			}
			if (fi.storePositionWithTermVector != storePositionWithTermVector) {
				fi.storePositionWithTermVector = true; // once vector, always
														// vector
			}
			if (fi.storeOffsetWithTermVector != storeOffsetWithTermVector) {
				fi.storeOffsetWithTermVector = true; // once vector, always
														// vector
			}
			if (fi.omitNorms != omitNorms) {
				fi.omitNorms = false; // once norms are stored, always store
			}
			if (fi.storePayloads != storePayloads) {
				fi.storePayloads = true;
			}

		}
		return fi;
	}

	private FieldInfo addInternal(String name, boolean isIndexed,
			boolean storeTermVector, boolean storePositionWithTermVector,
			boolean storeOffsetWithTermVector, boolean omitNorms,
			boolean storePayloads) {
		FieldInfo fi = new FieldInfo(name, isIndexed, byNumber.size(),
				storeTermVector, storePositionWithTermVector,
				storeOffsetWithTermVector, omitNorms, storePayloads);
		byNumber.add(fi);
		byName.put(name, fi);
		return fi;
	}

	public int fieldNumber(String fieldName) {
		FieldInfo fi = fieldInfo(fieldName);
		return (fi != null) ? fi.number : -1;
	}

	public FieldInfo fieldInfo(String fieldName) {
		return (FieldInfo) byName.get(fieldName);
	}

	/**
	 * Return the fieldName identified by its number.
	 * 
	 * @param fieldNumber
	 * @return the fieldName or an empty string when the field with the given
	 *         number doesn't exist.
	 */
	public String fieldName(int fieldNumber) {
		FieldInfo fi = fieldInfo(fieldNumber);
		return (fi != null) ? fi.name : "";
	}

	/**
	 * Return the fieldinfo object referenced by the fieldNumber.
	 * 
	 * @param fieldNumber
	 * @return the FieldInfo object or null when the given fieldNumber doesn't
	 *         exist.
	 */
	public FieldInfo fieldInfo(int fieldNumber) {
		return (fieldNumber >= 0) ? (FieldInfo) byNumber.get(fieldNumber)
				: null;
	}

	public int size() {
		return byNumber.size();
	}

	public boolean hasVectors() {
		boolean hasVectors = false;
		for (int i = 0; i < size(); i++) {
			if (fieldInfo(i).storeTermVector) {
				hasVectors = true;
				break;
			}
		}
		return hasVectors;
	}

	public void write(Directory d, String name) throws IOException {
		IndexOutput output = d.createOutput(name);
		try {
			write(output);
		} finally {
			output.close();
		}
	}

	public void write(IndexOutput output) throws IOException {
		output.writeVInt(size());
		for (int i = 0; i < size(); i++) {
			FieldInfo fi = fieldInfo(i);
			byte bits = 0x0;
			if (fi.isIndexed)
				bits |= IS_INDEXED;
			if (fi.storeTermVector)
				bits |= STORE_TERMVECTOR;
			if (fi.storePositionWithTermVector)
				bits |= STORE_POSITIONS_WITH_TERMVECTOR;
			if (fi.storeOffsetWithTermVector)
				bits |= STORE_OFFSET_WITH_TERMVECTOR;
			if (fi.omitNorms)
				bits |= OMIT_NORMS;
			if (fi.storePayloads)
				bits |= STORE_PAYLOADS;
			output.writeString(fi.name);
			output.writeByte(bits);
		}
	}

	private void read(IndexInput input) throws IOException {
		int size = input.readVInt();// read in the size
		for (int i = 0; i < size; i++) {
			String name = input.readString().intern();
			byte bits = input.readByte();
			boolean isIndexed = (bits & IS_INDEXED) != 0;
			boolean storeTermVector = (bits & STORE_TERMVECTOR) != 0;
			boolean storePositionsWithTermVector = (bits & STORE_POSITIONS_WITH_TERMVECTOR) != 0;
			boolean storeOffsetWithTermVector = (bits & STORE_OFFSET_WITH_TERMVECTOR) != 0;
			boolean omitNorms = (bits & OMIT_NORMS) != 0;
			boolean storePayloads = (bits & STORE_PAYLOADS) != 0;

			addInternal(name, isIndexed, storeTermVector,
					storePositionsWithTermVector, storeOffsetWithTermVector,
					omitNorms, storePayloads);
		}
	}

}
